//*********************************************************
//
//    Copyright (c) Microsoft. All rights reserved.
//    This code is licensed under the Apache License, Version 2.0.
//    THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF
//    ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY
//    IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR
//    PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.
//
//*********************************************************

namespace Microsoft.Research.eResearch.Blackboard
{
    using System;
    using System.Collections.Generic;
    using System.Collections.ObjectModel;
    using System.Globalization;
    using Microsoft.Research.eResearch.Blackboard.Common;
    using Microsoft.Research.eResearch.Blackboard.Interfaces;
    using Microsoft.Research.eResearch.Common.Eventing;
    using Microsoft.Research.eResearch.Common.Logging;
    using Microsoft.Research.eResearch.Common.Reflection;
    using Microsoft.Research.eResearch.Common.Threading;

    /// <summary>
    /// Represents the core functionality of the Blackboard system.  Creates instances of a number 
    /// of modules at startup that provide storage and communication services.
    /// The blackboard keeps track of a list of all subscribers and the keys they are interested in 
    /// (their subscription profile) as well as keeping a list of all publishers.  When a publisher
    /// sends a message to the Blackboard, it routes the appropriate messages to the subscribers 
    /// depending on their subscription profile.
    /// </summary>
    public class Blackboard : ILoggable
    {
        /// <summary>
        /// Instance of publisher message reciever.
        /// </summary>
        private IPublisherMessageReceiver publisherMessageReceiver;

        /// <summary>
        /// Instance of publisher subscription sender.
        /// </summary>
        private IPublisherSubscriptionSender publisherSubscriptionSender;

        /// <summary>
        /// Instance of notification message sender.
        /// </summary>
        private INotificationMessageSender notificationMessageSender;

        /// <summary>
        /// Instance of publisher message reciever.
        /// </summary>
        private INotificationSubscriptionReceiver notificationSubscriptionReceiver;

        /// <summary>
        /// Instance of publisher store.
        /// </summary>
        private IPublisherStore publisherStore;

        /// <summary>
        /// Instance of subscription store.
        /// </summary>
        private ISubscriptionStore subscriptionStore;

        /// <summary>
        /// Initializes a new instance of the <see cref="Blackboard"/> class.
        /// Creates new instances of the types described in the App.config file with the keys
        /// "IPublisherMessageReceiver", "IPublisherSubscriptionSender", "INotificationMessageSender", "INotificationSubscriptionReceiver",
        /// "IPublisherStore", and "ISubscriptionStore". 
        /// </summary>
        public Blackboard()
        {
            // Create instances of the plugins
            this.publisherMessageReceiver = ReflectionHelper.CreateInstance<IPublisherMessageReceiver>("IPublisherMessageReceiver");
            this.publisherSubscriptionSender = ReflectionHelper.CreateInstance<IPublisherSubscriptionSender>("IPublisherSubscriptionSender");
            this.notificationMessageSender = ReflectionHelper.CreateInstance<INotificationMessageSender>("INotificationMessageSender");
            this.notificationSubscriptionReceiver = ReflectionHelper.CreateInstance<INotificationSubscriptionReceiver>("INotificationSubscriptionReceiver");
            this.publisherStore = ReflectionHelper.CreateInstance<IPublisherStore>("IPublisherStore");
            this.subscriptionStore = ReflectionHelper.CreateInstance<ISubscriptionStore>("ISubscriptionStore");
            this.ConnectEvents();
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="Blackboard"/> class.
        /// This constructor allows you to specify the instances of the plugins.
        /// </summary>
        /// <param name="publisherMessageReceiver">The publisher message receiver.</param>
        /// <param name="publisherSubscriptionSender">The publisher subscription sender.</param>
        /// <param name="notificationMessageSender">The notification message sender.</param>
        /// <param name="notificationSubscriptionReceiver">The notification subscription receiver.</param>
        /// <param name="publisherStore">The publisher store.</param>
        /// <param name="subscriptionStore">The subscription store.</param>
        public Blackboard(
            IPublisherMessageReceiver publisherMessageReceiver,
            IPublisherSubscriptionSender publisherSubscriptionSender,
            INotificationMessageSender notificationMessageSender,
            INotificationSubscriptionReceiver notificationSubscriptionReceiver,
            IPublisherStore publisherStore,
            ISubscriptionStore subscriptionStore)
        {
            if (publisherMessageReceiver == null)
            {
                throw new ArgumentNullException("publisherMessageReceiver");
            }

            if (publisherSubscriptionSender == null)
            {
                throw new ArgumentNullException("publisherSubscriptionSender");
            }

            if (notificationMessageSender == null)
            {
                throw new ArgumentNullException("notificationMessageSender");
            }

            if (notificationSubscriptionReceiver == null)
            {
                throw new ArgumentNullException("notificationSubscriptionReceiver");
            }

            if (publisherStore == null)
            {
                throw new ArgumentNullException("publisherStore");
            }

            if (subscriptionStore == null)
            {
                throw new ArgumentNullException("publisherStore");
            }

            this.publisherMessageReceiver = publisherMessageReceiver;
            this.publisherSubscriptionSender = publisherSubscriptionSender;
            this.notificationMessageSender = notificationMessageSender;
            this.notificationSubscriptionReceiver = notificationSubscriptionReceiver;
            this.publisherStore = publisherStore;
            this.subscriptionStore = subscriptionStore;
            this.ConnectEvents();
        }

        /// <summary>
        /// Fired when a log message is received.
        /// </summary>
        public event EventHandler<LogEventArgs> LogMessageReceived;

        /// <summary>
        /// Starts the Blackboard instance.  Two listeners are started to receive information about publisher
        /// registration/messages and subscriber subscriptions.
        /// </summary>
        public void Start()
        {
            // Start the services
            notificationSubscriptionReceiver.Start();
            publisherMessageReceiver.Start();
        }

        /// <summary>
        /// Stops the Blackboard instance.  The two listeners are stopped and will not receive any further 
        /// communication from the publishers or subscribers.
        /// </summary>
        public void Stop()
        {
            // Stop the services
            if (notificationSubscriptionReceiver != null)
            {
                notificationSubscriptionReceiver.Stop();
            }

            if (publisherMessageReceiver != null)
            {
                publisherMessageReceiver.Stop();
            }
        }

        /// <summary>
        /// Raises the LogMessageReceived event.
        /// </summary>
        /// <param name="level">The log level.</param>
        /// <param name="message">The log message.</param>
        protected virtual void OnLogMessageReceived(string level, string message)
        {
            this.LogMessageReceived.Fire(this, new LogEventArgs(level, message));
        }

        /// <summary>
        /// Connects the events and event handlers for blackboard.
        /// </summary>
        private void ConnectEvents()
        {
            // Wire up the events
            publisherMessageReceiver.BlackboardMessageReceived += new EventHandler<BlackboardMessageReceivedEventArgs>(OnBlackboardMessageReceived);
            publisherMessageReceiver.PublisherRegistering += new EventHandler<PublisherRegisteringEventArgs>(OnPublisherRegistering);
            publisherMessageReceiver.PublisherUnregistering += new EventHandler<PublisherUnregisteringEventArgs>(OnPublisherUnregistering);
            notificationSubscriptionReceiver.NotificationSubscription += new EventHandler<NotificationSubscriptionEventArgs>(OnNotificationSubscription);
            notificationSubscriptionReceiver.NotificationUnsubscription += new EventHandler<NotificationUnsubscriptionEventArgs>(OnNotificationUnsubscription);
            subscriptionStore.SubscriptionStoreChanged += new EventHandler<EventArgs>(OnSubscriptionStoreChanged);
        }

        /// <summary>
        /// Raised whenever publisher is registering.
        /// </summary>
        /// <param name="sender">Instance of the publisher.</param>
        /// <param name="e">Instance of the registering event argument.</param>
        private void OnPublisherRegistering(object sender, PublisherRegisteringEventArgs e)
        {
            Log("Info", "New publisher registering: {0}", e.IncomingReturnAddress.ToString());
            publisherStore.Register(e.IncomingReturnAddress);
            e.OutgoingSubscriptionProfile = subscriptionStore.GetAggregateProfile();
            Log("Info", "Outgoing profile: Count={0}", e.OutgoingSubscriptionProfile.Count);
        }

        /// <summary>
        /// Raised whenever publisher is un-registering.
        /// </summary>
        /// <param name="sender">Instance of the publisher.</param>
        /// <param name="e">Instance of the un-registering event argument.</param>
        private void OnPublisherUnregistering(object sender, PublisherUnregisteringEventArgs e)
        {
            Log("Info", "Publisher unregistering: {0}", e.IncomingReturnAddress.ToString());
            publisherStore.Unregister(e.IncomingReturnAddress);
        }

        /// <summary>
        /// Raised whenever message is recieved from publisher.
        /// </summary>
        /// <param name="sender">Instance of the publisher.</param>
        /// <param name="e">Instance of the blackboard message event argument.</param>
        private void OnBlackboardMessageReceived(object sender, BlackboardMessageReceivedEventArgs e)
        {
            e.IncomingMessage.Add("trident:BBTimeStamp", System.DateTime.Now.ToString());
            Log("Debug", "Blackboard message received: {0}", e.IncomingMessage.ToString());
            IDictionary<Uri, BlackboardMessage> outgoingMessages = subscriptionStore.CreateNotificationMessages(e.IncomingMessage);

            foreach (KeyValuePair<Uri, BlackboardMessage> pair in outgoingMessages)
            {
                ThreadPoolHelper.QueueUserWorkItem(
                    new { Address = pair.Key, Message = pair.Value },
                    (input) =>
                    {
                        Uri address = input.Address;
                        BlackboardMessage message = new BlackboardMessage(input.Message);
                        try
                        {
                            Log("Debug", "Notifying subscriber: {0}", address.ToString());
                            notificationMessageSender.Notify(address, message);
                        }
                        catch (Exception exp)
                        {
                            // There was a problem sending the message, 
                            // so unsubscribe the Uri so we don't send messages there again
                            Log("Error", "Could not notify subscriber {0} due to exception: {1}. Removing from subscription list.", address, exp.Message);
                            subscriptionStore.Unsubscribe(address);
                        }
                    });
            }
        }

        /// <summary>
        /// Raised whenever a new subscriber is subscribing.
        /// </summary>
        /// <param name="sender">Instance of the subscriber.</param>
        /// <param name="e">Instance of the subscription event argument.</param>
        private void OnNotificationSubscription(object sender, NotificationSubscriptionEventArgs e)
        {
            Log("Info", "New subscriber subscribing: {0}", e.IncomingReturnAddress.ToString());
            subscriptionStore.Subscribe(e.IncomingReturnAddress, e.IncomingSubscriptionProfile);
        }

        /// <summary>
        /// Raised whenever subscriber is un-subscribing.
        /// </summary>
        /// <param name="sender">Instance of the subscriber.</param>
        /// <param name="e">Instance of the un-subscription event argument.</param>
        private void OnNotificationUnsubscription(object sender, NotificationUnsubscriptionEventArgs e)
        {
            Log("Info", "Subscriber unsubscribing: {0}", e.IncomingReturnAddress.ToString());
            subscriptionStore.Unsubscribe(e.IncomingReturnAddress);
        }

        /// <summary>
        /// Raised whenever subscription store is changed.
        /// </summary>
        /// <param name="sender">Instance of the blackboard.</param>
        /// <param name="e">Instance of the empty event argument.</param>
        private void OnSubscriptionStoreChanged(object sender, EventArgs e)
        {
            Log("Info", "SubscriptionStoreChanged event");
            SubscriptionProfile newProfile = subscriptionStore.GetAggregateProfile();
            ReadOnlyCollection<Uri> publishers = publisherStore.GetCurrentPublisherList();
            foreach (Uri publisher in publishers)
            {
                ThreadPoolHelper.QueueUserWorkItem(
                    new { Address = publisher, Profile = newProfile },
                    (input) =>
                    {
                        Uri address = input.Address;
                        SubscriptionProfile profile = input.Profile;
                        try
                        {
                            Log("Info", "Attempting to notify publisher {0} with new profile.", address);
                            publisherSubscriptionSender.UpdateSubscriptionProfile(address, profile);
                        }
                        catch (Exception)
                        {
                            // There was a problem sending to the publisher, so unregister that publisher
                            Log("Error", "Update to {0} failed.  Removing from publisher list.", address);
                            publisherStore.Unregister(address);
                        }
                    });
            }
        }

        /// <summary>
        /// Writes the log message.
        /// </summary>
        /// <param name="level">The log level.</param>
        /// <param name="message">Message string to be logged.</param>
        private void Log(string level, string message)
        {
            OnLogMessageReceived(level, String.Format(CultureInfo.CurrentUICulture, "Blackboard: {0}", message));
        }

        /// <summary>
        /// Writes the log message.
        /// </summary>
        /// <param name="level">The log level.</param>
        /// <param name="format">Format of the message.</param>
        /// <param name="args">Arguments to be formatted.</param>
        private void Log(string level, string format, params object[] args)
        {
            Log(level, String.Format(CultureInfo.CurrentUICulture, format, args));
        }

        ///// <summary>
        ///// Writes the log message.
        ///// </summary>
        ///// <param name="level">The log level.</param>
        ///// <param name="message">Message string to be logged.</param>
        //private void Log(string level, string message)
        //{
        //    OnLogMessageReceived(level, String.Format(CultureInfo.CurrentUICulture, "Blackboard: {0}", message));
        //}

        ///// <summary>
        ///// Writes the log message.
        ///// </summary>
        ///// <param name="level">The log level.</param>
        ///// <param name="format">Format of the message.</param>
        ///// <param name="args">Arguments to be formatted.</param>
        //private void Log(string level, string format, params object[] args)
        //{
        //    Log(level, String.Format(CultureInfo.CurrentUICulture, format, args));
        //}
    }
}
